#include "planet.h"
#include "planeteventarg.h"
#include "planeteventlistener.h"
#include "planetcell.h"
#include "planetitem.h"

#include "servicecamera.h"
#include "serviceentity.h"
#include "application.h"
#include "servicephysics.h"
#include "collectibleentity.h"

#include "object.h"
#include "entity.h"
#include "itementity.h"
#include "itemcraftboxentity.h"
#include "itemchestentity.h"

#include "serviceworld.h"
#include "worldgridmanager.h"

#include "terraingenerator.h"
#include "terraingeneratorlibnoise.h"
#include "terraingeneratorsimplex.h"

namespace infinity {

Planet::Planet(TerrainGenerator* generator, dw::int32 radius)
:	m_radius(radius)
,	m_generator(generator)
,	m_cells(NULL)
{
	dwAssert(generator != NULL, "Invalid parameter: generator is null");

	dw::uint32 arraySide = m_radius * 2;
	m_cells = new Cell [arraySide * arraySide * arraySide];
}

Planet::~Planet()
{
	dwAssert(m_listeners.empty(), "There are still registered listeners");

	delete [] m_cells;
}

void Planet::install()
{
	dw::maths::ivec3 pos;

	dw::uint32 arraySide = m_radius * 2;

	for (dw::uint y = 0; y < m_radius * 2; ++y)
	{
		pos.y = y - dw::int32(m_radius);
		for (dw::uint z = 0; z < m_radius * 2; ++z)
		{
			pos.z = z - dw::int32(m_radius);
			for (dw::uint x = 0; x < m_radius * 2; ++x)
			{
				pos.x = x - dw::int32(m_radius);
				m_cells[y * arraySide * arraySide + z * arraySide + x].setMaterialId(m_generator->getMaterial(pos));
			}
		}
	}
}

void Planet::term()
{
	m_cellUserDelta.clear();
}

void Planet::registerListener(EventListener* listener)
{
	dwAssert(listener != NULL, "Invalid parameter: listener == NULL");
	dwAssert(std::find(m_listeners.begin(), m_listeners.end(), listener) == m_listeners.end(), "Invalid parameter: listener is already registered");

	m_listeners.push_back(listener);

	EventArg arg;

	listener->onRegistered(*this, arg);
}

void Planet::unregisterListener(EventListener* listener)
{
	dwAssert(listener != NULL, "Invalid parameter: listener == NULL");
	dwAssert(std::find(m_listeners.begin(), m_listeners.end(), listener) != m_listeners.end(), "Invalid parameter: listener is not registered");

	m_listeners.erase(std::find(m_listeners.begin(), m_listeners.end(), listener));

	EventArg arg;

	listener->onUnregistered(*this, arg);
}

dw::int32 Planet::getRadius() const
{
	return m_radius;
}

void Planet::getCell(const Vector3i& coords, Cell& cell) const
{
	if (coords.x > m_radius - 1 ||
		coords.y > m_radius - 1 ||
		coords.z > m_radius - 1)
	{
		cell = Cell::Default;
		return ;
	}

	if (coords.x < -m_radius ||
		coords.y < -m_radius ||
		coords.z < -m_radius)
	{
		cell = Cell::Default;
		return ;
	}

	//! @todo check in areas if the cell has changed, if not :
	if (!getInCellUser(coords, cell))
	{
		dw::maths::ivec3 arrayCoords = coords + dw::maths::ivec3(m_radius, m_radius, m_radius);
		dw::uint32 arraySide = m_radius * 2;
		cell = m_cells[arrayCoords.y * arraySide * arraySide + arrayCoords.z * arraySide + arrayCoords.x];
		// generateCell(coords, cell);
	}
}

bool Planet::getItem(const Vector3i& coords, Item& item) const
{
	if (coords.x > m_radius - 1 ||
		coords.y > m_radius - 1 ||
		coords.z > m_radius - 1)
	{
		return false;
	}

	if (coords.x < -m_radius ||
		coords.y < -m_radius ||
		coords.z < -m_radius)
	{
		return false;
	}

	//! @todo check in areas if the cell has changed, if not :
	return getInItemUser(coords,item);
}

void Planet::boom(const Vector3i& coords, float radius, float power)
{
	Cell cellTmp;
	const WorldGridManager& worldManager = WorldGridManager::getInstance();
	const MaterialLibrary& materialLib = worldManager.getPlanet()->getMaterialLibrary();
	float energy = 0.f;
	float cellToughness = 0.f;


	//! @todo destroyed item !!!! 


	//float radiussquare = radius * radius;
	for(int x=-radius;x<=radius; x++)
		for(int y=-radius;y<=radius; y++)
			for(int z=-radius;z<=radius; z++)
			{
				getCell(coords + Vector3i(x,y,z),cellTmp);
				energy = SphereEval(x,y,z,power) * 4.f; //kikoulol variable 
				cellToughness = materialLib.getMaterialFromId(cellTmp.getMaterialId()).getToughness();
				if(energy > cellToughness)

					setCell(coords + Vector3i(x,y,z),Planet::Cell::Default);
			}
}
float Planet::SphereEval(int x,int y,int z, float power)
{
	float distance = (x*x + y*y + z*z);

	return power / (distance*distance);
}


//todo ==> power en fct de ce que l'on a dans la main pour connaitre la freq du spawn
void Planet::setCell(const Vector3i& coords, const Cell& cell, float power /*= 1.0f*/)
{
	if (coords.x > m_radius - 1 ||
		coords.y > m_radius - 1 ||
		coords.z > m_radius - 1)
	{
		return ;
	}

	if (coords.x < -m_radius ||
		coords.y < -m_radius ||
		coords.z < -m_radius)
	{
		return ;
	}

	dw::uint64 multiplicateur = m_radius*2;
	dw::uint64 x = coords.x + m_radius;
	dw::uint64 y = coords.y + m_radius;
	dw::uint64 z = coords.z + m_radius;

	dw::uint64 key = x + multiplicateur *  y +  multiplicateur *  multiplicateur * z;

	
	
	if(cell.getMaterialId() ==  Planet::Cell::Default.getMaterialId())
	{
		Cell cellDestroyed;
		getCell(coords,cellDestroyed);

		const WorldGridManager& worldManager = WorldGridManager::getInstance();
		const MaterialLibrary& materialLib = worldManager.getPlanet()->getMaterialLibrary();
		const Material& material = materialLib.getMaterialFromId(cellDestroyed.getMaterialId());

		if(material.getSpawnType() != Material::NOTHING )
		{
			
			ServiceCamera& svcCamera = Application::getInstance().getService< ServiceCamera >();
			dw::graphics::Node* cameraNode = svcCamera.getActiveCameraController()->getMatrix();
			const Vector3f direction(cameraNode->getTransform().at);

			// Spawn entitys
			ServiceEntity& serviceEntity = Application::getInstance().getService< ServiceEntity >();

			int nbSpawn = material.getSpawnNb();
			
			for(unsigned int i=0; i < nbSpawn;i++)
			{
				CollectibleEntity* entity = serviceEntity.spawnEntity< CollectibleEntity >("DestroyedBlock");

				Vector3f pos(coords[0], coords[1], coords[2]);

				entity->setInitialPosition(pos + Vector3f(0.5f, 0.5f, 0.5f));
				entity->setInitialVelocity(/*2.0f * direction +*/ Vector3f(rand()%3 - 1, 2.0f, rand()%3 - 1));
				entity->setTimeToLive(1.0f * 20.0f);
				if(material.getSpawnType() == Material::BLOC)
				{
					entity->setObject(Object::BLOC, material.getIdSpawn());
				}
				else if(material.getSpawnType() == Material::ITEM)
				{
					entity->setObject(Object::ENTITY, material.getIdSpawn());
				}
			}
		}
	}	

	setCellUser(key,cell);

	CellEventArg arg(coords, cell);

	onCellChanged(arg);
}

void Planet::setItem(const Vector3i& coords, const Item& item)
{
	if (coords.x > m_radius - 1 ||
		coords.y > m_radius - 1 ||
		coords.z > m_radius - 1)
	{
		return ;
	}

	if (coords.x < -m_radius ||
		coords.y < -m_radius ||
		coords.z < -m_radius)
	{
		return ;
	}

	dw::uint64 multiplicateur = m_radius*2;
	dw::uint64 x = coords.x + m_radius;
	dw::uint64 y = coords.y + m_radius;
	dw::uint64 z = coords.z + m_radius;

	dw::uint64 key = x + (multiplicateur *  y) +  (multiplicateur *  multiplicateur * z);


	/*if(item.getItemId() ==  Planet::Item::Default.getItemId())*/
	{
		ServiceCamera& svcCamera = Application::getInstance().getService< ServiceCamera >();
		dw::graphics::Node* cameraNode = svcCamera.getActiveCameraController()->getMatrix();
		const Vector3f direction(cameraNode->getTransform().at);

		// Spawn entity
		ServiceEntity& serviceEntity = Application::getInstance().getService< ServiceEntity >();

		ItemEntity* entity = NULL;
		if(item.getItemId() == 14)
		{
			entity = serviceEntity.spawnEntity< ItemCraftBoxEntity >("CraftBox");
		}
		else if(item.getItemId() == 15)
		{
			entity = serviceEntity.spawnEntity< ItemChestEntity >("chest");
		}
		else
		{
			entity = serviceEntity.spawnEntity< ItemEntity >("DefaultItem");
		}
		
		
		Vector3f pos(coords[0], coords[1], coords[2]);

		entity->setPosition(pos );
	}

	setItemUser(key,item);
}


void Planet::removeItem(const Vector3i& coords)
{
	if (coords.x > m_radius - 1 ||
		coords.y > m_radius - 1 ||
		coords.z > m_radius - 1)
	{
		return ;
	}

	if (coords.x < -m_radius ||
		coords.y < -m_radius ||
		coords.z < -m_radius)
	{
		return ;
	}

	dw::uint64 multiplicateur = m_radius*2;
	dw::uint64 x = coords.x + m_radius;
	dw::uint64 y = coords.y + m_radius;
	dw::uint64 z = coords.z + m_radius;

	dw::uint64 key = x + (multiplicateur *  y) +  (multiplicateur *  multiplicateur * z);


	//collect item

	std::vector< Entity * >& livingEntity = Application::getInstance().getService< ServiceEntity >().getLivingEntity();
	std::vector< Entity * >::const_iterator entitiesEnd = livingEntity.end();

	Entity* entity = NULL;
	ItemEntity* itemEntity = NULL;
	dw::maths::Vector4f coordsV4 = dw::maths::Vector4f(coords.x,coords.y,coords.z,1.0f);

	for (std::vector< Entity * >::iterator entityIt = livingEntity.begin(); entityIt != entitiesEnd; ++entityIt)
	{
 		Entity* entity = *entityIt;
 		if(entity->getType() >= Entity::Type_Item )
		{
			itemEntity = static_cast<ItemEntity*>(entity);
			if(itemEntity->getMatrix().pos == coordsV4)
			{
				Item itemDestroyed;
				getItem(coords,itemDestroyed);

				Application::getInstance().getService< ServiceEntity >().condemnEntity(itemEntity);

				ServiceCamera& svcCamera = Application::getInstance().getService< ServiceCamera >();
				dw::graphics::Node* cameraNode = svcCamera.getActiveCameraController()->getMatrix();
				const Vector3f direction(cameraNode->getTransform().at);

				// Spawn entity
				ServiceEntity& serviceEntity = Application::getInstance().getService< ServiceEntity >();

				CollectibleEntity* entity = serviceEntity.spawnEntity< CollectibleEntity >("DestroyedBlock");

				Vector3f pos(coords[0], coords[1], coords[2]);

				entity->setInitialPosition(pos + Vector3f(0.5f, 0.5f, 0.5f));
				entity->setInitialVelocity(2.0f * direction + Vector3f(0.0f, 2.0f, 0.0f));
				entity->setTimeToLive(1.0f * 20.0f);
				entity->setObject(Object::ENTITY_BLOC, itemDestroyed.getItemId());
			}
		}
	}

	m_itemUser.erase(key);
}


void Planet::setCellUser(const dw::int64 key, const Cell& cell)
{
	m_cellUserDelta[key] = cell;
}

void Planet::setItemUser(const dw::int64 key, const Item& item)
{
	m_itemUser[key] = item;
}

void Planet::onCellChanged(CellEventArg& arg)
{
	for (ListenersCollection::iterator it = m_listeners.begin(); it != m_listeners.end(); ++it)
	{
		(*it)->onCellChanged(*this, arg);
	}
}

void Planet::generateCell(const Vector3i& coords, Cell& cell) const
{
	cell.setMaterialId(m_generator->getMaterial(coords));
}

bool Planet::getInCellUser(const Vector3i& coords, Cell& cell) const
{
	dw::uint64 multiplicateur = m_radius*2;
	dw::uint64 x = coords.x + m_radius;
	dw::uint64 y = coords.y + m_radius;
	dw::uint64 z = coords.z + m_radius;

	dw::uint64 key = x + (multiplicateur *  y) +  (multiplicateur *  multiplicateur * z);
	
	CellUserDeltaIterator it = m_cellUserDelta.find(key);

	if (it != m_cellUserDelta.end())
	{
		cell = it->second;
		return true;
	}

	return false;
}

bool Planet::getInItemUser(const Vector3i& coords, Item& item) const
{
	dw::uint64 multiplicateur = m_radius*2;
	dw::uint64 x = coords.x + m_radius;
	dw::uint64 y = coords.y + m_radius;
	dw::uint64 z = coords.z + m_radius;

	dw::uint64 key = x + (multiplicateur *  y) +  (multiplicateur *  multiplicateur * z);

	ItemUserIterator it = m_itemUser.find(key);

	if (it != m_itemUser.end())
	{
		item = it->second;
		return true;
	}

	return false;
}

void Planet::save(const char* filePath)
{
	FILE* file = NULL;
	file = fopen(filePath, "w+");

	if (file != NULL)
	{
		fprintf(file, "%d \n", m_radius);

		/*
		fprintf(file, "%d \n", m_maxAltitude);
		fprintf(file, "%d \n", m_maxDepth);
		*/
		
		//cellUser
		fprintf(file, "%d \n", m_cellUserDelta.size());
		for (CellUserDeltaIterator it = m_cellUserDelta.begin(); it != m_cellUserDelta.end(); ++it)
		{
			dw::int64 key =  it->first;
			int id = it->second.getMaterialId();
			fprintf(file, "%ld ", key);
			fprintf(file, "%d", id);
			fprintf(file, "\n");
		}

		//ItemUser
		fprintf(file, "%d \n", m_itemUser.size());
		for (ItemUserIterator it = m_itemUser.begin(); it != m_itemUser.end(); ++it)
		{
			dw::int64 key =  it->first;
			int id = it->second.getItemId();
			fprintf(file, "%ld ", key);
			fprintf(file, "%d", id);
			fprintf(file, "\n");
		}
		fclose(file);
	}
	
}

/*static*/ Planet* Planet::load(const char* filePath)
{
	FILE* file = NULL;
	file = fopen(filePath, "r");

	Planet* planet = NULL;

	if (file != NULL)
	{

		dw::int32 radius;
		dw::int32 maxAltitude;
		dw::int32 maxDepth;

		fscanf(file, "%d", &radius);
		fscanf(file, "%d", &maxAltitude);
		fscanf(file, "%d", &maxDepth);

		// planet = new Planet(new TerrainGeneratorLibNoise(radius, maxAltitude, maxDepth), radius);
		planet = new Planet(new TerrainGeneratorSimplex(64, 32), 64);

		//cellUser
		int nbUserData;
		fscanf(file, "%d", &nbUserData);
		for (int i = 0; i < nbUserData;i++)
		{
			dw::int64 key = 0;
			int id = 0;
			fscanf(file, "%ld %d", &key, &id);
			
			Cell tmpCell;
			tmpCell.setMaterialId(id);
			planet->setCellUser(key,tmpCell);
		}

		//itemUser
		int nbItemData;
		fscanf(file, "%d", &nbItemData);
		for (int i = 0; i < nbItemData;i++)
		{
			dw::int64 key = 0;
			int id = 0;
			fscanf(file, "%ld %d", &key, &id);

			Item tmpItem;
			tmpItem.setItemId(id);

//			depack key
			dw::uint64 multiplicateur = radius*2;
			dw::uint64 z = floor(float(key / ( multiplicateur *  multiplicateur)));
			dw::uint64 y = floor(float((key-(z*multiplicateur *  multiplicateur)) / multiplicateur));
			dw::uint64 x = key-(y*multiplicateur) - (z*multiplicateur*multiplicateur);

			x -= radius;
			y -= radius;
			z -= radius;

			planet->setItem(Vector3i(x,y,z),tmpItem);
			planet->setItemUser(key,tmpItem);

		}
		fclose(file);
	}
	
	return planet;
}

} // namespace infinity
